// ==UserScript==
// @name         收集BestSeller排名
// @namespace    http://tampermonkey.net/
// @version      0.1
// @description  try to take over the world!
// @author       You
// @updateURL    https://gitee.com/great-pacey/tampermonkey/raw/master/best.seller.user.js
// @match        https://www.walmart.com/*
// @icon         https://www.google.com/s2/favicons?domain=walmart.com
// @require      https://code.jquery.com/jquery-3.6.0.min.js
// @grant        GM_registerMenuCommand
// @grant        unsafeWindow
// @grant        GM_xmlhttpRequest
// ==/UserScript==
/** global $ */

(async function () {
    const ip = `127.0.0.1`
    const action_time = '15:43';
    let press_count = 0, total_count;
    let continueFlag = true;
    let stopFlag = false;
    const requestFailUrl = [];
    GM_registerMenuCommand('开始', main);

    let MAP = {};

    async function requestUrls() {
        const url = `http://${ip}:3000/BestSeller`;
        return new Promise(resolve => {
            GM_xmlhttpRequest({
                method: 'GET',
                url,
                onload: function(res) {
                    resolve(JSON.parse(res.responseText));
                }
            })
        })
    }

    function sendSever(products, url) {
        const { year, month, date, hour, minute } = genNowDateObj();
        const data = products.map(o => {
            const { productId, IsSponsored, Promote, FulfillmentBadges, IsSaveWithWFS, Price, Reviews, Title, Description, No } = o;
            const time = `${formatNum(hour)}:${formatNum(minute)}`;
            return {
                keyword: MAP[url], url, No, productId, time,
                IsSponsored, Promote, FulfillmentBadges, IsSaveWithWFS,
                Price, Reviews, Title, Description, date, month,
                year, hour, fullDate: `${year}-${month}-${date} ${time}`
            };
        });

        return new Promise(resolve => {
            GM_xmlhttpRequest({
                method: "POST",
                url: url.includes('sort=best_seller') ? `http://${ip}:3000/bestSellerProduct` : `http://${ip}:3000/bestMatchProduct`,
                headers: { 'Content-Type': 'application/json' },
                data: JSON.stringify(data),
                onload: resolve
            });
        })
    }

    async function main() {
        searchLoop();
    }

    function genMap(obj) {
        Object.keys(obj).forEach(key => {
            obj[key].forEach(url => {
                MAP[url] = key;
            })
        })
    }

    async function getUrlMap() {
        const { BestSeller, BestMatch } = await requestUrls();
        genMap(BestSeller); genMap(BestMatch);
        total_count = 2 * Object.keys(BestSeller).reduce((p, n) => p + BestSeller[n].length, 0);
        return { BestSeller, BestMatch };
    }

    function searchLoop() {
        const [action_h, action_m] = action_time.split(/:|：/);

        async function loop() {
            const [h, m] = getUSATime();
            if (h === action_h && m === action_m) {
                const map = await getUrlMap();
                searchWordInBulk(map);
            } else {
                console.log('BestSeller等待开始', markTime());
                setTimeout(loop, 59*1000);
            }
        }
        loop();
    }

    function formatNum(n) {
        return n > 9 ? String(n) : '0' + n;
    }

    async function searchWord(url) {
        let products;
        try {
            if (stopFlag) return;
            products = await requestSort(url);
            if (!products.length) {
                requestFailUrl.push(url);
                return;
            }
            products = products.filter(i => i.__typename === "Product");
            products = formatProduct(products);
            sendSever(products, url);
            console.log(`${markTime()}，${url}请求成功`, `${++press_count}/${total_count}`);
        } catch (error) {
            console.log(products);
        }
    }

    async function searchWordInBulk(map) {
        const { BestMatch, BestSeller } = map;
        async function loop(urls) {
            if (!urls.length) {
                console.log(requestFailUrl, '请求未完成');
                await researchFailWord();
                press_count = 0;
                console.log(`已经完成`);
                console.log('=======================');
                await notifyEnd();
                searchLoop();
                return;
            };
            await searchWord(urls.shift());
            await wait(randomNum(1200, 2000));
            loop(urls);
        }

        function getUrlsFromMap(obj) {
            let result = [];
            Object.keys(obj).forEach(key => {
                result = result.concat(obj[key]);
            })
            return result;
        }
        await loop(getUrlsFromMap(BestMatch).concat(getUrlsFromMap(BestSeller)));
    }

    async function notifyEnd() {
        return new Promise(resolve => {
            GM_xmlhttpRequest({
                method: "POST",
                url: `http://${ip}:3000/end`,
                onload: resolve
            });
        })
    }

    async function researchFailWord() {
        async function loop() {
            if (!requestFailUrl.length) return;
            await searchWord(requestFailUrl.shift());
            await wait(randomNum(800, 1200));
            loop();
        }
        loop();
    }

    async function wait(num) {
        return new Promise(resolve => {
            setTimeout(resolve, num);
        });
    }

    function formatProduct(products) {
        return products.map((i, n) => {
            return {
                No: n + 1,
                productId: i.usItemId,
                Price: String(i.price) || String(i.priceInfo.minPriceForVariant),
                IsSponsored: normalization(i.sponsoredProduct),
                Promote: i.flag,
                IsSaveWithWFS: normalization(i.fulfillmentIcon),
                FulfillmentBadges: i.fulfillmentBadges.join(';'),
                Reviews: i.rating.numberOfReviews,
                Title: i.name,
                Description: i.description
            };
        });
    }

    function normalization(param) {
        return param ? 'Y' : '';
    }

    function requestSort(url) {
        return new Promise((resolve) => {
            GM_xmlhttpRequest({
                method: "GET",
                url,
                onload: async function (res) {
                    let result;
                    try {
                        const reg = /<script\b(?:\s+[^>]*)?__NEXT_DATA__(?:[^>]*)?>(.*?)<\/script\s*>/i;
                        const text = res.responseText.match(reg)[1];
                        result = JSON.parse(text).props.pageProps.initialData.searchResult.itemStacks[0].items;
                    } catch (err) {
                        if (isRobotVerification(res.responseText)) {
                            await requestSendEmail();
                            await pauseToWaitVerification();
                            result = await requestSort(url);
                        } else {
                            console.log(`${url}异常，html: `, res.responseText);
                        }
                    } finally {
                        resolve(result);
                    }
                },
                onerror: async function (res) {
                    console.log(`${url}请求触发error`, res);
                    resolve([]);
                }
            });
        })
    }

    function pauseToWaitVerification() {
        continueFlag = false;
        insertElement();
        return new Promise(resolve => {
            (function loop() {
                const fn = continueFlag ? resolve : loop;
                setTimeout(fn, 950);
            })();
        });
    }

    function insertElement() {
        const el = $("<div><h1>机器验证后点我</h1></div>");
        el.css({width: '80%', height: '80%', position: 'fixed', zIndex: 200, left: '10%', top: '10%', background: 'pink'})
        el.click(function() {
            continueFlag = true;
            el.remove();
        })
        $('body').append(el);
    }

    function requestSendEmail() {
        return new Promise(resolve => {
            GM_xmlhttpRequest({
                method: "POST",
                url: `http://${ip}:3000/sendEmail`,
                data: 'send email',
                onload: resolve
            });
        })
    }

    function genNowDateObj() {
        const d = new Date();
        const year = d.getFullYear();
        const month = d.getMonth() + 1;
        const date = d.getDate();
        const hour = d.getHours();
        const minute = d.getMinutes();
        return { year, month, date, hour, minute };
    }

    function markTime() {
        const date = new Date();
        return date.getHours() + ':' + date.getMinutes();
    }

    function randomNum(minNum, maxNum) {
        switch (arguments.length) {
            case 1:
                return parseInt(Math.random() * minNum + 1, 10);
            case 2:
                return parseInt(Math.random() * (maxNum - minNum + 1) + minNum, 10);
            default:
                return 0;
        }
    }

    function isRobotVerification(html) {
        return html.includes('<title>Robot or human?</title>');
    }

    function getUSATime() {
        const d = new Date();
        return [d.getHours(), d.getMinutes()].map(formatNum);
    }
})();